package com.yst.web.utils;

import lombok.Data;
import lombok.experimental.Accessors;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;

import java.lang.reflect.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * class utils
 */
public class ClassUtils {

    private static final String getPrefix="get";
    private static final String setPrefix="set";
    private static final Map<String,Class> wrapperClass=new HashMap<>(8);

    @Data
    @Accessors(chain = true)
    static class MethodField{
        private Method getMethod;
        private Method setMethod;
    }

    private static final Map<String,Map<String, MethodField>> methodFieldMap;

    private static final Map<String,Map<String, Field>> fieldMap;

    private static final Map<Class<?>,Object> classSingletonMap;

    private static final Map<Class<?>,Map<String,List<Method>>> classMethodMap;

    private static final List<Integer> ignoreModifier=new ArrayList<>();


    static {
        methodFieldMap=Collections.synchronizedMap(new HashMap<>());
        fieldMap=Collections.synchronizedMap(new HashMap<>());
        classSingletonMap=Collections.synchronizedMap(new HashMap<>());
        classMethodMap=Collections.synchronizedMap(new HashMap<>());

        ignoreModifier.add(Modifier.STATIC);
        ignoreModifier.add(Modifier.ABSTRACT);
        ignoreModifier.add(Modifier.FINAL);
        ignoreModifier.add(Modifier.NATIVE);

        wrapperClass.put(boolean.class.getSimpleName(), Boolean.class);
        wrapperClass.put(float.class.getSimpleName(), Float.class);
        wrapperClass.put(int.class.getSimpleName(), Integer.class);
        wrapperClass.put(long.class.getSimpleName(), Long.class);
        wrapperClass.put(char.class.getSimpleName(), Character.class);
        wrapperClass.put(short.class.getSimpleName(), Short.class);
        wrapperClass.put(double.class.getSimpleName(), Double.class);
        wrapperClass.put(byte.class.getSimpleName(), Byte.class);
    }

    public static boolean isIgnoreModifies(int modifies){
        for(int modify:ignoreModifier){
            if ((modify & modifies)!=0){
                return true;
            }
        }
        return false;
    }

    private static void getSupperClassFields(Class<?> clazz,Map<String,Field> outMap){
        if (clazz.getSuperclass()==null||clazz.getSuperclass()==Object.class) return;
        if (ignoreModifier.contains(clazz.getModifiers())){
            return;
        }
        getSupperClassFields(clazz.getSuperclass(),outMap);
        Field[] fields=clazz.getSuperclass().getDeclaredFields();
        for(Field field:fields){
            if (!isIgnoreModifies(field.getModifiers())) {
                outMap.put(field.getName(), field);
            }
        }
    }
    /**
     * Fetch the fields of the class
     * @param clazz source class
     * @return field of class in map
     */
    public static Map<String,Field> getClassFields(Class<?> clazz){
        if (Objects.isNull(clazz)) return null;
        if (fieldMap.containsKey(clazz.getName())){
            return Collections.unmodifiableMap(fieldMap.get(clazz.getName()));
        }
        else
        {
            Map<String,Field> map=new LinkedHashMap<>();
            getSupperClassFields(clazz,map);
            Field[] fields=clazz.getDeclaredFields();
            for(Field field:fields){
                if (!isIgnoreModifies(field.getModifiers())) {
                    map.put(field.getName(), field);
                }
            }
            fieldMap.put(clazz.getName(),map);
            return Collections.unmodifiableMap(map);
        }
    }

    /**
     * Get a field of the specified type from the class
     * @param clazz source class
     * @param fieldClass class of field
     * @return Fields that meet the criteria
     */
    public static List<Field> getClassFieldsByFieldClass(Class<?> clazz,Class<?> fieldClass){
        List<Field> fieldList=new ArrayList<>();
        Map<String,Field> classFields=getClassFields(clazz);
        for (Field field:classFields.values()){
            if (field.getType().isAssignableFrom(fieldClass)){
                fieldList.add(field);
            }
        }
        return fieldList;
    }

    /**
     * fetch get set method of field for class
     * @param clazz source class
     * @return
     */
    public static Map<String,MethodField> getClassFieldMethods(Class<?> clazz){
        if (Objects.isNull(clazz)) return null;
        if (methodFieldMap.containsKey(clazz.getName())){
            return Collections.unmodifiableMap(methodFieldMap.get(clazz.getName()));
        }
        else
        {
            Map<String,MethodField> map=new LinkedHashMap<>();
            Method[] methods=clazz.getDeclaredMethods();
            for(Method method:methods){
                String mName=method.getName();
                if (mName.startsWith(getPrefix)) {
                    mName=mName.substring(getPrefix.length());
                    MethodField methodField=new MethodField();
                    methodField.getMethod=method;
                    String setMethodName=setPrefix+mName;
                    try {
                        methodField.setMethod = clazz.getMethod(setMethodName, method.getReturnType());
                    }
                    catch (Exception ignore){
                    }
                }
            }
            methodFieldMap.put(clazz.getName(),map);
            return Collections.unmodifiableMap(map);
        }
    }

    /**
     * fetch singleton object by class
     * @param clazz class
     * @param <T> T
     * @return singleton object
     * @throws Exception exception
     */
    public static <T> T getSingletonObject(Class<T> clazz) throws Exception{
        if (classSingletonMap.containsKey(clazz)){
            return (T)classSingletonMap.get(clazz);
        }
        else
        {
            T data = clazz.newInstance();
            classSingletonMap.put(clazz,data);
            return data;
        }
    }

    /**
     * String to any
     * @param value source string
     * @param clazz class of target
     * @param <T> T
     * @return target
     * @throws Exception exception
     */
    public static <T> T convertToObject(String value, Class<T> clazz) throws Exception {
        if (clazz.isPrimitive()){
            if (clazz.isAssignableFrom(String.class)) return (T)value;
            Method method=clazz.getDeclaredMethod("valueOf",String.class);
            return (T)method.invoke(clazz,value);
        }
        else
        {
            return JsonConverter.getJsonConverter().parseObject(value, clazz);
        }
    }

    public static <T> List<T> convertToList(String value,Class<T> elementClazz) throws Exception {
        return JsonConverter.getJsonConverter().parseList(value,elementClazz);
    }


    /**
     * determine an class is an wrapper class
     * @param clazz source class
     * @return true is a wrapper class,false is not a wrapper class
     */
    public static Boolean isWrapperClass(Class clazz){
        try {
            return ((Class) clazz.getField("TYPE").get(null)).isPrimitive();
        }
        catch (Exception e){
            //ignore error
        }
        return false;
    }

    /**
     * determine an object is a primitive object
     * @param object source object
     * @return true is a primitive object,false is not a primitive object
     */
    public static Boolean isPrimitive(Object object){
        return isPrimitive(object.getClass());
    }

    /**
     * determine an class is a primitive class or is a wrapper class
     * @param clazz class
     * @return true is base class
     */
    public static Boolean isPrimitive(Class<?> clazz){
        return clazz.isPrimitive()||isWrapperClass(clazz);
    }



    /**
     * fetch class from type
     * @param type source type
     * @return class map to type
     */
    public static Class<?> fromType(Type type){
        try {
            if (type instanceof ParameterizedTypeImpl){
                return fromType(((ParameterizedTypeImpl) type).getActualTypeArguments()[0]);
            }
            else {
                if (((Class)type).isPrimitive()) {
                    return wrapperClass.get(type.getTypeName());
                }
                else {
                    return Class.forName(type.getTypeName());
                }
            }
        }
        catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    /**
     * determine class have specific field,even if have only one
     * @param clazz source class
     * @param fields array of field
     * @return true have,false don't have
     */
    public static Boolean classExistsField(Class<?> clazz,String...fields){
        Map<String,Field> fieldMap=getClassFields(clazz);
        for(String field:fields) {
            return fieldMap.containsKey(field);
        }
        return false;
    }

    /**
     * fetch method from class by name of method
     * @param clazz class
     * @param methodName name of method
     * @return list of method
     */
    public static List<Method> getMethods(Class<?> clazz,String methodName){
        if (classMethodMap.containsKey(clazz)){
            return classMethodMap.get(clazz).getOrDefault(methodName, Collections.emptyList());
        }
        else {
            Map<String,List<Method>> map=new HashMap<>();
            classMethodMap.put(clazz,map);
            Method[] methods=clazz.getDeclaredMethods();
            for (Method method:methods){
                List<Method> methodList;
                if (map.containsKey(method.getName())){
                   methodList=map.get(method.getName());
                }
                else {
                   methodList=new ArrayList<>();
                   map.put(methodName,methodList);
                }
                methodList.add(method);
            }
            return map.getOrDefault(methodName,Collections.emptyList());
        }
    }

    /**
     * fetch static method from class by name of method
     * @param clazz class
     * @param methodName name of method
     * @return list of method
     */
    public static List<Method> getStaticMethods(Class<?> clazz,String methodName){
        List<Method> methods=getMethods(clazz,methodName);
        List<Method> result=new ArrayList<>();
        for(Method method:methods){
            if (Modifier.isStatic(method.getModifiers())){
                result.add(method);
            }
        }
        return Collections.unmodifiableList(result);
    }

    /**
     * determine an class have specific method by name of method
     * @param clazz class
     * @param methodName name of method
     * @return true=have，false=don't have
     */
    public static boolean haveStaticMethod(Class<?> clazz,String methodName){
        List<Method> methods=getMethods(clazz,methodName);
        for(Method method:methods){
            if (Modifier.isStatic(method.getModifiers())){
                return true;
            }
        }
        return false;
    }

    /**
     * fetch type of parameter for method
     * @param method method
     * @param paramIndex index of parameter
     * @return class of parameter
     */
    public static Class<?> getMethodParamClass(Method method,int paramIndex){
        Parameter[] parameters= method.getParameters();
        if (paramIndex< parameters.length){
            return parameters[paramIndex].getType();
        }
        return null;
    }

    /**
     * determine an class inherit from specific array of class
     * @param source class of source
     * @param classes specific array of class
     * @return true,false
     */
    public static boolean isAssignableFrom(Class<?> source,Class<?>...classes){
        for(Class<?> c:classes){
            if (c.isAssignableFrom(source)) return true;
        }
        return false;
    }

    /**
     * fetch method from object by name of method and parameters
     * @param object source object
     * @param methodName name of method
     * @param args array of parameter
     * @return method
     */
    public static Method getMethod(Object object,String methodName,Object[] args){
        if (Objects.isNull(object)) return null;
        try {
            return object.getClass().getDeclaredMethod(methodName, Arrays.stream(args).map(Object::getClass)
                    .collect(Collectors.toList()).toArray(new Class[]{}));
        }
        catch (Exception e){
            e.printStackTrace();
            //ignore error
        }
        return null;
    }

    private final static Map<Class<?>,Integer> depthCache=new ConcurrentHashMap<>();

    /**
     * fetch depth which between class and Object.class
     * @param clazz class
     * @return depth
     */
    public static int getClassDepth(Class<?> clazz){
        if (Object.class.isAssignableFrom(clazz)){
            return 0;
        }

        //use state machine,prevent stack overflow
        //I can't determine use recursion,but it's very quickly
        return depthCache.computeIfAbsent(clazz,(key)->{
            Class<?> parent=key.getSuperclass();
            int result=1;
            while (Objects.nonNull(parent)){
                if (parent.equals(Object.class)){
                    break;
                }
                parent=parent.getSuperclass();
                result++;
            }
            return result;
        });
    }

    public static boolean classImplInterface(Class<?> clazz,Class<?>...interfaces){
        List<Class<?>> list=Arrays.asList(interfaces);
        return Arrays.stream(clazz.getInterfaces()).anyMatch(list::contains);
    }

    public static List<Class<?>> getTypeGenericParams(Type type){
        List<Class<?>> result=new LinkedList<>();
        if (type instanceof ParameterizedType){
            result.add(fromType(((ParameterizedType) type).getRawType()));
            Type[] types=((ParameterizedType)type).getActualTypeArguments();
            for (Type t:types){
                result.addAll(getTypeGenericParams(t));
            }
        }
        else {
            result.add(fromType(type));
        }
        return result;
    }

    public static List<Class<?>> getFieldGenericTypeChain(Field field) {
        Type type = field.getGenericType();
        List<Class<?>> result = new LinkedList<>(getTypeGenericParams(type));
        return result;
    }

    public static Class<?> getReturnGenericClass(Integer paramIndex,Class<?>...parametersType) throws ClassNotFoundException, NoSuchMethodException {
        StackTraceElement[] stackTraceElements= Thread.currentThread().getStackTrace();
        if (stackTraceElements.length<2){
            return null;
        }
        StackTraceElement stackTraceElement=stackTraceElements[2];
        String className=stackTraceElement.getClassName();
        String methodName=stackTraceElement.getMethodName();
        Class<?> clazz=Class.forName(className);
        Method method=clazz.getMethod(methodName,parametersType);
        if (method.getGenericReturnType() instanceof ParameterizedTypeImpl){
            return getTypeGenericParams(method.getGenericReturnType()).get(paramIndex);
        }
        return null;
    }

    public static Class<?> getReturnGenericClass(Class<?>...parametersType) throws ClassNotFoundException, NoSuchMethodException {
        StackTraceElement[] stackTraceElements= Thread.currentThread().getStackTrace();
        if (stackTraceElements.length<2){
            return null;
        }
        StackTraceElement stackTraceElement=stackTraceElements[2];
        String className=stackTraceElement.getClassName();
        String methodName=stackTraceElement.getMethodName();
        Class<?> clazz=Class.forName(className);
        Method method=clazz.getMethod(methodName,parametersType);
        if (method.getGenericReturnType() instanceof ParameterizedTypeImpl){
            return getTypeGenericParams(method.getGenericReturnType()).get(1);
        }
        return null;
    }

    public static Class<?> getReturnClass(Object callObj,Class<?>...parametersType) throws ClassNotFoundException, NoSuchMethodException {
        StackTraceElement[] stackTraceElements= Thread.currentThread().getStackTrace();
        if (stackTraceElements.length<2){
            return null;
        }
        StackTraceElement stackTraceElement=stackTraceElements[2];
        String methodName=stackTraceElement.getMethodName();
        Class<?> clazz=callObj.getClass();
        Method method=clazz.getDeclaredMethod(methodName,parametersType);
        return method.getReturnType();
    }

    public static List<Class<?>> getGenericClass(Object object){
        Type type=object.getClass().getGenericSuperclass();
        if (type instanceof  ParameterizedType){
            return getTypeGenericParams(type);
        }
        return Collections.emptyList();
    }

    /**
     * fetch classes generic expression
     * @param clazz source class
     * @return list of generic class
     */
    public static List<Class<?>> getGenericClass(Class<?> clazz){
        Type type=clazz.getGenericSuperclass();
        if (type instanceof  ParameterizedType){
            return getTypeGenericParams(type);
        }
        return Collections.emptyList();
    }

}
